# 
#  PySL:  Use S-Lang from Python (and vice versa)
#  Copyright (C) 2005,2006 Christopher J. Stawarz <chris@pseudogreen.org>
# 
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
# 
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
# 
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
# 


import sys
from operator import getitem, setitem, delitem
import unittest
import numpy
import pysl
from pysl import sl


LONG_IS_INT = (numpy.int_(0).itemsize == numpy.intc(0).itemsize)
SLANG_LLONG_IS_LONG = (sl.__class_id(sl.LLong_Type) ==
                       sl.__class_id(sl.Long_Type))



class TestCase(unittest.TestCase):

    def to_slstr(obj):
        s = repr(obj)
        if isinstance(obj, basestring):
            s = s[1:-1].replace('"', r'\"')
            s = '"' + s + '"'        
        return s
    to_slstr = staticmethod(to_slstr)

    def assertSLangEqual(self, got, expect):
        pysl.load_string('_eqs(%s, %s)' % (got, expect))
        self.assertEqual(pysl.pop(), True)

    def assertSLangRaises(self, slerr, slcode):
        testcode = ("""
          try
          {
            %s;
            0;
          }
          catch %s:
          {
            1;
          }
        """ % (slcode, slerr))

        pysl.load_string(testcode)
        self.assert_(pysl.pop())
        self.assert_(sl._stkdepth() == 0)


class TestErrors(TestCase):

    def test_simple_slerror(self):
        self.assertRaises(pysl.SLangError, pysl.load_string, 'NULL + 2')

    def test_simple_pyerror(self):
        self.assertSLangRaises('PythonError', 'py_eval("print 3")')

    # FIXME: This test raises a "Num Args Stack Underflow"
    # error, presumably because libpysl_slang_to_python_error() is
    # calling SLang_restart() before we actually get back to top level
    # or is calling it too many times (i.e. more than once).
    #def test_nested_slerror(self):
    #    self.assertSLangRaises('PythonError',
    #                           'py.pysl.load_string("NULL + 2")')

    def test_nested_pyerror(self):
        self.assertRaises(pysl.SLangError, pysl.load_string,
                          'py_eval("print 3")')


class TestSLangObject(TestCase):

    def setUp(self):
        pysl.load_string("""
          $0 = struct { a, b };
          $0.a = 3;
          $0.b = "flam";
          $1 = ["foo", "bar"];
          $2 = Assoc_Type[];
          $2["foo"]=3;
          $2["bar"]=16.0;
          define echo() { }
          $0;
          $1;
          $2;
          &echo;
        """)

        self.echo = pysl.pop()
        self.assoc = pysl.pop()
        self.array = pysl.pop()
        self.struct = pysl.pop()

    def test_str(self):
        self.assertEqual(str(self.array), 'String_Type[2]')

    def test_getattr(self):
        # '__' attribute
        self.assert_(self.assoc.__class__ is pysl.SLangObject)
        self.assertRaises(AttributeError, getattr, self.assoc, '__foo__')

        # Normal attribute
        self.assertEqual(self.struct.a, 3)
        self.assertRaises(pysl.SLangError, getattr, self.struct, 'shazam')
        self.assertRaises(pysl.SLangError, getattr, self.assoc, 'shazam')

    def test_setattr(self):
        # '__' attribute
        self.assertRaises(AttributeError, setattr, self.assoc, '__foo__', 3)
        self.assertRaises(AttributeError, setattr, self.assoc, '__str__', 3)

        # Normal attribute
        self.assertRaises(AttributeError, delattr, self.struct, 'a')
        self.struct.b = -2.0
        self.assertEqual(self.struct.b, -2.0)
        self.struct.b = 'flam'
        self.assertRaises(pysl.SLangError, setattr, self.assoc, 'a', 3)

    def test_length(self):
        self.assertEqual(len(self.array), 2)

    def test_index(self):
        self.assertEqual([s for s in self.array], ['foo', 'bar'])

        # No guarantee on order in which we get keys
        vals = [self.assoc[s] for s in self.assoc]
        vals.sort()
        self.assertEqual(vals, [3, 16.0])

    def test_subscript(self):
        self.assertEqual(self.assoc['foo'], 3)
        self.assertRaises(pysl.SLangError, getitem, self.struct, 'foo')

    def test_ass_subscript(self):
        self.assertEqual(self.assoc['foo'], 3)
        self.assertRaises(pysl.SLangError, getitem, self.struct, 'foo')

        self.assertRaises(TypeError, delitem, self.assoc, 'foo')
        self.assoc['foo'] = 'jammy'
        self.assertEqual(self.assoc['foo'], 'jammy')
        self.assoc['foo'] = 3

    def test_call(self):
        self.assertRaises(TypeError, self.echo, foo=3)
        self.assertRaises(pysl.SLangError, self.assoc)
        pysl.load_string('&$0')
        self.assertRaises(pysl.SLangError, pysl.pop())

        self.assertEqual(self.echo(), None)
        self.assertEqual(self.echo(1), 1)
        self.assertEqual(self.echo(1,2), (1,2))


class TestPyObject(TestCase):

    def setUp(self):
        class C(object):  pass
        self.o = C()
        self.o.spam = 3
        self.l = [1, 2.0, 'foo']
        self.d = {'a': 4.0, 2: 'bar', 4.0: 3}
        def f(*args, **kwargs):
            return str(args) + ' ' + str(kwargs)
        self.f = f
        def f2(*args):
            if len(args) == 0:
                return None
            if len(args) == 1:
                return args[0]
            return args
        self.f2 = f2

        pysl.push(self.o)
        pysl.push(self.l)
        pysl.push(self.d)
        pysl.push(self.f)
        pysl.push(self.f2)
        
        pysl.load_string("""
          variable func2 = ();
          variable func = ();
          variable dict = ();
          variable list = ();
          variable obj = ();
        """)

    def tearDown(self):
        pysl.load_string("""
          __uninitialize(&func2);
          __uninitialize(&func);
          __uninitialize(&dict);
          __uninitialize(&list);
          __uninitialize(&obj);
        """)

    def test_string(self):
        self.assertSLangEqual('string(list)', '"%s"' % self.l)

    def test_length(self):
        if sl._slang_version >= 20005:
            self.assertSLangEqual('length(list)', '3')
            self.assertSLangRaises('PythonError', 'length(func)')

    def test_call(self):
        # Bad usage
        self.assertSLangRaises('UsageError', 'py_call()')
        self.assertSLangRaises('TypeMismatchError', 'py_call(3)')

        # No args
        self.assertSLangEqual('py_call(func)', self.to_slstr(self.f()))

        # Regular args only
        self.assertSLangEqual('py_call(func, 2, 4.0)',
                              self.to_slstr(self.f(2, 4.0)))

        # Keyword args only
        self.assertSLangEqual('py_call(func, PY_KW, "foo", 2, "bar", 4.0)',
                              self.to_slstr(self.f(foo=2, bar=4.0)))

        # Missing keyword arg
        self.assertSLangRaises('StackUnderflowError',
                               'py_call(func, PY_KW, "foo", 2, "bar")')

        # Regular and keyword args
        self.assertSLangEqual('py_call(func, 2, 4.0, PY_KW, "foo", 2, ' +
                              '"bar", 4.0)',
                              self.to_slstr(self.f(2, 4.0, foo=2, bar=4.0)))

        # No return val
        pysl.load_string('py_call(func2)')
        self.assert_(sl._stkdepth() == 0)

        # One return val
        pysl.load_string('py_call(func2, 3); $0 = ();')
        self.assertSLangEqual('$0', '3')
        self.assert_(sl._stkdepth() == 0)

        # Multiple return vals
        pysl.load_string('py_call(func2, 3, 4); ($0, $1) = ();')
        self.assertSLangEqual('$0', '3')
        self.assertSLangEqual('$1', '4')
        self.assert_(sl._stkdepth() == 0)

    def test_sget(self):
        self.assertSLangEqual('obj.spam', '3')
        self.assertSLangRaises('PythonError', 'obj.ham')

    def test_sput(self):
        pysl.load_string('obj.spam = 4')
        self.assertEqual(self.o.spam, 4)
        self.o.spam = 3
        self.assertSLangRaises('PythonError', 'dict.spam = 4')

    def test_aget(self):
        # Not indexable
        self.assertSLangRaises('PythonError', 'func[3]')

        # One index
        self.assertSLangEqual('list[1]', '2.0')
        self.assertSLangEqual('dict["a"]', '4.0')

        # Multiple indices
        self.d[(1,2,3)] = 13
        self.assertSLangEqual('dict[1,2,3]', '13')
        del self.d[(1,2,3)]

    def test_aput(self):
        # Not indexable
        self.assertSLangRaises('PythonError', 'func[3] = 7')

        # One index
        pysl.load_string('dict["yum"] = 12')
        self.assertEqual(self.d['yum'], 12)
        del self.d['yum']

        # Multiple indices
        pysl.load_string('dict[1,2,3] = 7')
        self.assertEqual(self.d[(1,2,3)], 7)
        del self.d[(1,2,3)]

    def test_foreach(self):
        pysl.load_string('variable item;')

        def _foreach(name):
            slcode = ("""
              foreach item (%s) {
                item;
              }
            """ % name)
            return slcode
        
        # 'foreach using ...'
        self.assertSLangRaises('NotImplementedError', """
          foreach item (list) using ("keys") {
             % Do nothing
          }
        """)

        # Non-iterable
        self.assertSLangRaises('PythonError', _foreach('func'))

        # Iterable
        pysl.load_string(_foreach('list'))
        self.assertEqual(pysl.pop(), 'foo')
        self.assertEqual(pysl.pop(), 2.0)
        self.assertEqual(pysl.pop(), 1)
        self.assert_(sl._stkdepth() == 0)

        pysl.load_string('__uninitialize(&item);')


class TestArrayScalar(TestCase):

    def _test(self, pytype, sltypestr, inval=1, checktype=None):
        if not checktype:
            checktype = pytype
        invalslstr = self.to_slstr(inval)
        
        pysl.load_string('typecast(%s, %s)' % (invalslstr, sltypestr))
        outval = pysl.pop()
        self.assert_(type(outval) is checktype)
        self.assertEqual(outval, inval)

        pysl.push(pytype(inval))
        pysl.load_string('$0 = (); string(typeof($0));')
        self.assertEqual(pysl.pop(), sltypestr)        
        pysl.load_string('$0 == ' + invalslstr)
        self.assertEqual(pysl.pop(), True)
                
    def test_byte(self):
        self._test(numpy.byte, 'Char_Type')
        
    def test_ubyte(self):
        self._test(numpy.ubyte, 'UChar_Type')
    
    def test_bool(self):
        self._test(numpy.bool_, 'UChar_Type', checktype=numpy.ubyte)

    def test_short(self):
        self._test(numpy.short, 'Short_Type')
        
    def test_ushort(self):
        self._test(numpy.ushort, 'UShort_Type')
        
    def test_int(self):
        self._test(numpy.intc, 'Integer_Type')
        
    def test_uint(self):
        self._test(numpy.uintc, 'UInteger_Type')
        
    def test_long(self):
        if LONG_IS_INT:
            sltype = 'Integer_Type'
            ctype = numpy.intc
        else:
            sltype = 'Long_Type'
            ctype = None
        self._test(numpy.int_, sltype, checktype=ctype)
        
    def test_ulong(self):
        if LONG_IS_INT:
            sltype = 'UInteger_Type'
            ctype = numpy.uintc
        else:
            sltype = 'ULong_Type'
            ctype = None
        self._test(numpy.uint, sltype, checktype=ctype)

    def test_longlong(self):
        if SLANG_LLONG_IS_LONG:
            sltype = 'Long_Type'
            ctype = numpy.int_
        else:
            sltype = 'LLong_Type'
            ctype = None
        self._test(numpy.longlong, sltype, checktype=ctype)
        
    def test_ulonglong(self):
        if SLANG_LLONG_IS_LONG:
            sltype = 'ULong_Type'
            ctype = numpy.uint
        else:
            sltype = 'ULLong_Type'
            ctype = None
        self._test(numpy.ulonglong, sltype, checktype=ctype)
        
    def test_float(self):
        self._test(numpy.single, 'Float_Type')
        
    def test_double(self):
        self._test(numpy.float_, 'Double_Type')
        
    def test_cfloat(self):
        self._test(numpy.csingle, 'Complex_Type', 1+2j, numpy.complex_)

    def test_cdouble(self):
        self._test(numpy.complex_, 'Complex_Type', 1+2j)
        
    def test_string(self):
        self._test(numpy.str_, 'String_Type', 'foo', str)

    def test_other(self):
        val = numpy.unicode_('foo')
        
        pysl.push(val)
        pysl.load_string('$0 = (); string(typeof($0));')
        self.assertEqual(pysl.pop(), 'PyObject')
        
        pysl.load_string('$0')
        self.assert_(pysl.pop() is val)


class TestArray(TestCase):

    def _test(self, pytype, sltypestr, inval=[1, 2, 4], checktype=None):
        if not checktype:
            checktype = pytype
        invalslstr = self.to_slstr(inval)

        pysl.load_string('typecast(%s, %s)' % (invalslstr, sltypestr))
        outval = pysl.pop()
        self.assert_(outval.dtype.type is checktype)
        self.assert_(numpy.all(outval == numpy.array(inval, pytype)))

        pysl.push(numpy.array(inval, pytype))
        pysl.load_string('$0 = (); string(typeof($0)); string(_typeof($0));')
        self.assertEqual(pysl.pop(), sltypestr)
        self.assertEqual(pysl.pop(), 'Array_Type')        
        pysl.load_string('_eqs($0, %s)' % invalslstr)
        self.assertEqual(pysl.pop(), True)
                
    def test_byte(self):
        self._test(numpy.byte, 'Char_Type')
        
    def test_ubyte(self):
        self._test(numpy.ubyte, 'UChar_Type')
    
    def test_bool(self):
        self._test(numpy.bool_, 'UChar_Type', [0, 1, 0], numpy.ubyte)

    def test_short(self):
        self._test(numpy.short, 'Short_Type')
        
    def test_ushort(self):
        self._test(numpy.ushort, 'UShort_Type')
        
    def test_int(self):
        self._test(numpy.intc, 'Integer_Type')
        
    def test_uint(self):
        self._test(numpy.uintc, 'UInteger_Type')
        
    def test_long(self):
        if LONG_IS_INT:
            sltype = 'Integer_Type'
            ctype = numpy.intc
        else:
            sltype = 'Long_Type'
            ctype = None
        self._test(numpy.int_, sltype, checktype=ctype)
        
    def test_ulong(self):
        if LONG_IS_INT:
            sltype = 'UInteger_Type'
            ctype = numpy.uintc
        else:
            sltype = 'ULong_Type'
            ctype = None
        self._test(numpy.uint, sltype, checktype=ctype)

    def test_longlong(self):
        if SLANG_LLONG_IS_LONG:
            sltype = 'Long_Type'
            ctype = numpy.int_
        else:
            sltype = 'LLong_Type'
            ctype = None
        self._test(numpy.longlong, sltype, checktype=ctype)
        
    def test_ulonglong(self):
        if SLANG_LLONG_IS_LONG:
            sltype = 'ULong_Type'
            ctype = numpy.uint
        else:
            sltype = 'ULLong_Type'
            ctype = None
        self._test(numpy.ulonglong, sltype, checktype=ctype)
        
    def test_float(self):
        self._test(numpy.single, 'Float_Type')
        
    def test_double(self):
        self._test(numpy.float_, 'Double_Type')
        
    def test_cfloat(self):
        self._test(numpy.csingle, 'Complex_Type', [1+2j, 4+8j, 16+32j],
                   numpy.complex_)
        
    def test_cdouble(self):
        self._test(numpy.complex_, 'Complex_Type', [1+2j, 4+8j, 16+32j])
        
    def test_pyobject(self):
        val = numpy.array(['foo', 'bar', 'blah'], numpy.str_)
        
        pysl.push(val)
        pysl.load_string('$0 = (); string(typeof($0));')
        self.assertEqual(pysl.pop(), 'PyObject')
        
        pysl.load_string('$0')
        self.assert_(pysl.pop() is val)

    def test_slangobject(self):
        pysl.load_string('$0 = ["foo", "bar", "blah"]; $0;')
        val = pysl.pop()
        self.assert_(type(val) is pysl.SLangObject)

        pysl.push(val)
        pysl.load_string('$1 = (); __is_same($0, $1);')
        self.assertEqual(pysl.pop(), True)

    def test_unwrap(self):
        # NumPy contiguous
        val = numpy.array([1, 2, 4])
        pysl.push(val)
        #self.assert_(pysl.pop() is val)
        self.assert_(pysl.pop() is not val)

        # NumPy non-contiguous
        val = numpy.array([0, 1, 0, 2, 0, 4])[::2]
        pysl.push(val)
        newval = pysl.pop()
        self.assert_(newval is not val)
        self.assert_(numpy.all(newval == val))

        # S-Lang
        pysl.load_string('$0 = [1, 2, 4]; $0;')
        val = pysl.pop()
        pysl.push(val)
        pysl.load_string('$1 = (); __is_same($0, $1);')
        #self.assertEqual(pysl.pop(), True)
        self.assertEqual(pysl.pop(), False)


class TestOtherTypes(TestCase):

    def _test(self, inval, sltypestr, invalslstr=None, checktype=None):
        if not invalslstr:
            invalslstr = self.to_slstr(inval)
        if not checktype:
            checktype = type(inval)
            
        pysl.load_string('typecast(%s, %s)' % (invalslstr, sltypestr))
        outval = pysl.pop()
        self.assert_(type(outval) is checktype)
        self.assertEqual(outval, inval)

        pysl.push(inval)
        pysl.load_string('$0 = (); string(typeof($0));')
        self.assertEqual(pysl.pop(), sltypestr)        
        pysl.load_string('$0 == ' + invalslstr)
        self.assertEqual(pysl.pop(), True)

    def test_none(self):
        self._test(None, 'Null_Type', 'NULL')

    def test_bool(self):
        self._test(True, 'Char_Type', '1', numpy.byte)
        self._test(False, 'Char_Type', '0', numpy.byte)
        
    def test_int(self):
        if LONG_IS_INT:
            sltype = 'Integer_Type'
            ctype = numpy.intc
        else:
            sltype = 'Long_Type'
            ctype = numpy.int_
        self._test(1, sltype, checktype=ctype)

    def test_long(self):
        # Fits in a long
        if LONG_IS_INT:
            sltype = 'Integer_Type'
            ctype = numpy.intc
        else:
            sltype = 'Long_Type'
            ctype = numpy.int_
        too_big = 1L << (8 * numpy.int_(0).itemsize - 1)            
        self._test(too_big-1, sltype, checktype=ctype)

        # Fits in a long long
        if not SLANG_LLONG_IS_LONG:
            too_big = 1L << (8 * numpy.longlong(0).itemsize - 1)
            self._test(too_big-1, 'LLong_Type', repr(too_big-1) + 'L',
                       numpy.longlong)

        # Must be wrapped in a PyObject
        pysl.push(too_big)
        pysl.load_string('$0 = (); string(typeof($0));')
        self.assertEqual(pysl.pop(), 'PyObject')        
        pysl.load_string('$0')
        self.assert_(pysl.pop() is too_big)

    def test_float(self):
        self._test(1.0, 'Double_Type', checktype=numpy.float_)

    def test_complex(self):
        self._test(1+2j, 'Complex_Type', checktype=numpy.complex_)

    def test_string(self):
        self._test('foo', 'String_Type')

    def test_zero_dim_array(self):
        self._test(numpy.array(1, numpy.intc), 'Integer_Type', '1', numpy.intc)

    def test_pyobject(self):
        val = u'foo'
        pysl.push(val)
        pysl.load_string('$0 = (); string(typeof($0));')
        self.assertEqual(pysl.pop(), 'PyObject')        
        pysl.load_string('$0')
        self.assert_(pysl.pop() is val)

    def test_slangobject(self):
        pysl.load_string('$0 = Assoc_Type; $0;')
        val = pysl.pop()
        self.assert_(type(val) is pysl.SLangObject)
        pysl.push(val)
        pysl.load_string('$1 = (); __is_same($0, $1);')
        self.assertEqual(pysl.pop(), True)


def run(verbosity=2):
    suite = unittest.TestSuite()
    
    for case in dir(sys.modules[__name__]):
        case = getattr(sys.modules[__name__], case)
        if isinstance(case, type) and issubclass(case, TestCase):
            suite.addTest(unittest.makeSuite(case))

    pysl.load_string('import("pysl")')

    old_py2sl_errormap = pysl.py2sl_errormap.copy()
    old_sl2py_errormap = pysl.sl2py_errormap.copy()

    try:
        pysl.py2sl_errormap.clear()
        pysl.sl2py_errormap.clear()
        unittest.TextTestRunner(verbosity=verbosity).run(suite)
    finally:
        pysl.py2sl_errormap.update(old_py2sl_errormap)
        pysl.sl2py_errormap.update(old_sl2py_errormap)
